package org.xx.armory.spring5.mvc;

import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.ModelAndView;
import org.xx.armory.spring5.data.PageArgs;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.UNICODE_CASE;
import static org.apache.commons.lang3.StringUtils.containsAny;
import static org.apache.commons.lang3.StringUtils.stripToEmpty;
import static org.apache.commons.lang3.StringUtils.substring;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.StringUtils.truncate;
import static org.apache.commons.lang3.math.NumberUtils.toInt;
import static org.xx.armory.commons.StringConverter.join;
import static org.xx.armory.commons.TextUtils.splitByLast;
import static org.xx.armory.commons.Validators.rejectIfEmpty;
import static org.xx.armory.commons.Validators.rejectIfNull;

public final class ModelUtils {
    public static final String JSON_VIEW = "jsonTemplate";
    public static final String SINGLE_BEAN_NAME = "bean";
    private static final char ARG_DELIMITER = '-';
    private static final int DEFAULT_PAGE_SIZE = 20;

    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]([a-zA-Z0-9_.-]*?[a-zA-Z0-9_-]+)?@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", CASE_INSENSITIVE | UNICODE_CASE | DOTALL);
    private static final Pattern MOBILE_PATTERN = Pattern.compile("^(1[1357-9]\\d{9})$", CASE_INSENSITIVE | UNICODE_CASE | DOTALL);
    private static final Pattern ID_CARD_PATTERN = Pattern.compile("^([0-9]{17}[0-9X])|([0-9]{15})$", CASE_INSENSITIVE | UNICODE_CASE | DOTALL);

    /**
     * 身份证校验码，用于验证身份证号码是否合法。
     *
     * @see #ID_CARD_WEIGHT
     */
    private final static String ID_CARD_VALID_CODES = "10X98765432";

    /**
     * 身份证校验码权重，用于验证身份证号码是否合法。
     *
     * @see #ID_CARD_VALID_CODES
     */
    private final static int[] ID_CARD_WEIGHT = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};

    /**
     * 无效的AutoAction字符。
     */
    private static final String ILLEGAL_AUTO_ACTION = "'\"&~!<>()[]=\\/";

    private ModelUtils() {
        throw new AssertionError();
    }

    public static ModelAndView buildJsonView(
            Object bean
    ) {
        final ModelAndView result = new ModelAndView(JSON_VIEW);

        if (bean != null) {
            result.addObject(SINGLE_BEAN_NAME, bean);
        }

        return result;
    }

    /**
     * 按照指定的参数名列表解析参数字符串，并返回参数名和值的映射。
     *
     * @param s
     *         待解析的参数字符串，如果是 {@code null} 则看作 空字符串。
     * @param firstArgName
     *         第一个参数名，自动去掉首尾空格。
     * @param otherArgNames
     *         其它参数名， 自动去掉首尾空格。
     * @return 解析后的参数名值映射，如果参数 {@code s} 的长度不足，那么未能解析的参数的值是空字符串。
     * @throws IllegalArgumentException
     *         如果参数 {@code firstArgName} 或者 {@code otherArgsName} 是 {@code null} 或者空字符串；
     *         或者参数名去掉首尾空格后存在同名。
     */
    public static ModelMap parseArgs(
            String s,
            String firstArgName,
            String... otherArgNames
    ) {
        s = trimToEmpty(s);

        final var argNames = new ArrayList<String>();
        var argName = trimToEmpty(firstArgName);
        if (argName.isEmpty()) {
            throw new IllegalArgumentException("firstArgName cannot be null or empty");
        }
        argNames.add(argName);
        final var otherArgNamesCount = otherArgNames != null ? otherArgNames.length : 0;
        for (var i = 0; i < otherArgNamesCount; ++i) {
            argName = trimToEmpty(otherArgNames[i]);
            if (argName.isEmpty()) {
                throw new IllegalArgumentException("otherArgNames cannot be null or empty");
            }
            if (argNames.contains(argName)) {
                throw new IllegalArgumentException(argName + " already exists");
            }
            argNames.add(argName);
        }

        final var ret = new ModelMap();

        var pos = 0;
        var argIndex = 0;
        final var value = new StringBuilder();
        while (pos < s.length() && argIndex < argNames.size()) {
            char ch = s.charAt(pos);
            switch (ch) {
                case ARG_DELIMITER:
                    // 发现分界符，当前参数解析完毕。
                    ret.put(argNames.get(argIndex++), value.toString());
                    value.setLength(0);
                    ++pos;
                    break;
                case '"':
                    // 发现双引号，需要检查是否存在分界符转义。
                    if (pos <= s.length() - 3 && s.charAt(pos + 1) == ARG_DELIMITER && s.charAt(pos + 2) == '"') {
                        // 是分界符转义序列，将分界符加入当前参数。
                        value.append(ARG_DELIMITER);
                        pos += 3;
                    } else {
                        // 不是分界符转义序列，将双引号作为普通字符加入当前参数。
                        value.append(ch);
                        ++pos;
                        break;
                    }
                    break;
                default:
                    // 其它字符，计入当前参数。
                    value.append(ch);
                    ++pos;
                    break;
            }
        }

        if (value.length() != 0 && argIndex < argNames.size()) {
            ret.put(argNames.get(argIndex++), value.toString());
        }

        // 将剩余的参数值都设置为空字符串。
        for (var i = argIndex; i < argNames.size(); ++i) {
            ret.put(argNames.get(i), "");
        }

        return ret;
    }

    /**
     * 解析查询分页参数。
     * <pre>
     * ""        -&gt; [0, default]
     * ",20"     -&gt; [0, 20]
     * ","       -&gt; [0, default]
     * "5,"      -&gt; [5, default]
     * "788"     -&gt; [788, default]
     * "788,800" -&gt; [788, 800]
     * </pre>
     *
     * @param range
     *         字符串形式的分页参数，如果是 {@code null} 则看作 空字符串。
     * @return 查询起始记录序号和最多返回的记录数。
     * @see #DEFAULT_PAGE_SIZE
     */
    public static PageArgs parsePageArgs(
            String range
    ) {
        range = trimToEmpty(range);

        final var dp = range.indexOf(',');
        final String s1;
        final String s2;
        if (dp != -1) {
            s1 = substring(range, 0, dp);
            s2 = substring(range, dp + 1);
        } else {
            s1 = range;
            s2 = "";
        }

        return PageArgs.of(toInt(s1, 0), toInt(s2, DEFAULT_PAGE_SIZE));
    }

    /**
     * 检查已绑定的请求参数是否不是 {@code null}。
     * <p>如果待检查的参数是 {@code null}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static <T> T paramNotNull(
            T value,
            String name
    ) {
        if (value == null) {
            throw new IllegalRequestParamException(name, "must not be null");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否不是空字符串。
     * <p>如果待检查的参数是 {@code null}，或者只包含空白字符，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static String paramNotBlank(
            String value,
            String name
    ) {
        if (value == null || value.isBlank()) {
            throw new IllegalRequestParamException(name, "must not be null or blank");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否不是空字符串，并且截断到指定长度。
     * <p>如果待检查的参数是 {@code null}，或者只包含空白字符，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param maxSize
     *         参数最大长度。
     * @return 参数值本身。
     * @see org.apache.commons.lang3.StringUtils#truncate(String, int)
     */
    public static String paramNotBlankAndTruncate(
            String value,
            String name,
            int maxSize
    ) {
        return truncate(paramNotBlank(value, name), maxSize);
    }

    /**
     * 检查已绑定的请求参数是否不为空。
     * <p>如果待检查的参数是 {@code null}，或者不包含任何元素，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <E>
     *         参数的元素类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static <E> Collection<? extends E> paramNotEmpty(
            Collection<? extends E> value,
            String name
    ) {
        if (value == null || value.isEmpty()) {
            throw new IllegalRequestParamException(name, "must not be null or empty");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否非负。
     * <p>如果待检查的参数是负数，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static int paramNonNegative(
            int value,
            String name
    ) {
        if (value < 0) {
            throw new IllegalRequestParamException(name, "must not be negative");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否正数。
     * <p>如果待检查的参数不是正数，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static int paramPositive(
            int value,
            String name
    ) {
        if (value <= 0) {
            throw new IllegalRequestParamException(name, "must be positive");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否非负。
     * <p>如果待检查的参数是负数，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static long paramNonNegative(
            long value,
            String name
    ) {
        if (value < 0) {
            throw new IllegalRequestParamException(name, "must not be negative");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否正数。
     * <p>如果待检查的参数不是正数，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static long paramPositive(
            long value,
            String name
    ) {
        if (value <= 0) {
            throw new IllegalRequestParamException(name, "must be positive");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否非负。
     * <p>如果待检查的参数是负数，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static BigDecimal paramNonNegative(
            BigDecimal value,
            String name
    ) {
        return paramGreaterThanOrEqual(value, BigDecimal.ZERO, name);
    }

    /**
     * 检查已绑定的请求参数是否正数。
     * <p>如果待检查的参数不是正数，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @return 参数值本身。
     */
    public static BigDecimal paramPositive(
            BigDecimal value,
            String name
    ) {
        return paramGreaterThan(value, BigDecimal.ZERO, name);
    }

    /**
     * 检查已绑定的请求参数是否大于指定的值。
     * <p>如果待检查的参数是 {@code null}，或者小于等于{@code min}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param min
     *         指定的最小值。
     * @return 参数值本身。
     */
    public static <T extends Comparable<T>> T paramGreaterThan(
            T value,
            T min,
            String name
    ) {
        rejectIfNull(min, "min");

        if (value == null || value.compareTo(min) <= 0) {
            throw new IllegalRequestParamException(name, "must greater than " + min);
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否大于或者等于指定的值。
     * <p>如果待检查的参数是 {@code null}，或者小于{@code min}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param min
     *         指定的最小值。
     * @return 参数值本身。
     */
    public static <T extends Comparable<T>> T paramGreaterThanOrEqual(
            T value,
            T min,
            String name
    ) {
        rejectIfNull(min, "min");

        if (value == null || value.compareTo(min) < 0) {
            throw new IllegalRequestParamException(name, "must greater than or equal " + min);
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否小于指定的值。
     * <p>如果待检查的参数是 {@code null}，或者大于等于{@code min}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param max
     *         指定的最小值。
     * @return 参数值本身。
     */
    public static <T extends Comparable<T>> T paramLess(
            T value,
            T max,
            String name
    ) {
        rejectIfNull(max, "min");

        if (value == null || value.compareTo(max) >= 0) {
            throw new IllegalRequestParamException(name, "must less than " + max);
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否小于或者等于指定的值。
     * <p>如果待检查的参数是 {@code null}，或者大于等于{@code min}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param max
     *         指定的最小值。
     * @return 参数值本身。
     */
    public static <T extends Comparable<T>> T paramLessThanOrEqual(
            T value,
            T max,
            String name
    ) {
        rejectIfNull(max, "min");

        if (value == null || value.compareTo(max) > 0) {
            throw new IllegalRequestParamException(name, "must less than or equal " + max);
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否是允许的值。
     * <p>如果待检查的参数是 {@code null}，或者不在集合{@code set}中，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param set
     *         指定的允许值集合。
     * @return 参数值本身。
     */
    public static <T> T paramInSet(
            T value,
            Set<T> set,
            String name
    ) {
        rejectIfNull(set, "set");
        rejectIfEmpty(set, "set");

        if (value == null || !set.contains(value)) {
            throw new IllegalRequestParamException(name, "must in [" + join(",", set.stream()) + "]");
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否等于期望的值。
     * <p>如果待检查的参数是 {@code null}，或者不是期望的值{@code expected}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param expected
     *         期望的值。
     * @return 参数值本身。
     */
    public static <T> T paramEqual(
            T value,
            T expected,
            String name
    ) {
        if (value == null || !value.equals(expected)) {
            throw new IllegalRequestParamException(name, "must equal " + expected);
        }

        return value;
    }

    /**
     * 检查已绑定的请求参数是否<strong>不</strong>是期望的值。
     * <p>如果待检查的参数<strong>是</strong>期望的值{@code notExpected}，那么抛出 {@link IllegalRequestParamException} 异常。</p>
     *
     * @param <T>
     *         参数类型。
     * @param name
     *         参数名字。
     * @param value
     *         待检查的参数值。
     * @param notExpected
     *         不期望的值。
     * @return 参数值本身。
     */
    public static <T> T paramNotEqual(
            T value,
            String name,
            T notExpected
    ) {
        if (Objects.equals(value, notExpected)) {
            throw new IllegalRequestParamException(name, "must not equal " + notExpected);
        }

        return value;
    }

    public static String paramLength(
            CharSequence value,
            String name,
            int min,
            int max
    ) {
        if (value == null || value.length() < min || value.length() > max) {
            throw new IllegalRequestParamException(name, "Length must be [" + min + ", " + max + "]");
        }

        return value.toString();
    }

    public static String paramLength(
            CharSequence value,
            String name,
            int min
    ) {
        if (value == null || value.length() < min) {
            throw new IllegalRequestParamException(name, "Length must larger than " + min);
        }

        return value.toString();
    }

    public static <E> Collection<E> paramLength(
            Collection<E> value,
            String name,
            int min,
            int max
    ) {
        if (value == null || value.size() < min || value.size() > max) {
            throw new IllegalRequestParamException(name, "Length must be [" + min + ", " + max + "]");
        }

        return value;
    }

    public static <E> Collection<E> paramLength(
            Collection<E> value,
            String name,
            int min
    ) {
        if (value == null || value.size() < min) {
            throw new IllegalRequestParamException(name, "Length must larger than " + min);
        }

        return value;
    }

    public static String paramPattern(
            CharSequence value,
            String name,
            Pattern pattern
    ) {
        rejectIfNull(pattern, "pattern");

        if (value == null || !pattern.matcher(value).matches()) {
            throw new IllegalRequestParamException(name, "must match " + pattern);
        }

        return value.toString();
    }

    /**
     * 校验手机号码。
     * <ul>
     *     <li>必须以数字{@literal 1}开头。</li>
     *     <li>第二位数字必须是{@literal 1,3,5,7,8,9}</li>
     *     <li>只能包含数字，长度必须是{@literal 11}位。</li>
     * </ul>
     *
     * @param value
     *         待校验的值。
     * @param name
     *         参数名。
     * @return 如果校验通过则返回待校验的值本身。a
     * @throws IllegalRequestParamException
     *         如果校验不通过。
     */
    public static String paramMobile(
            CharSequence value,
            String name
    ) {
        return paramPattern(value, name, MOBILE_PATTERN);
    }

    public static String paramEmail(
            CharSequence value,
            String name
    ) {
        return paramPattern(value, name, EMAIL_PATTERN);
    }

    public static String paramIdCard(
            CharSequence value,
            String name
    ) {
        if (value == null || !ID_CARD_PATTERN.matcher(value).matches()) {
            throw new IllegalRequestParamException(name, "must match " + ID_CARD_PATTERN);
        }

        // 逐位计算校验码。
        final var sum = IntStream.range(0, ID_CARD_WEIGHT.length)
                                 .reduce(0, (a, i) -> a + ID_CARD_WEIGHT[i] * (value.charAt(i) - '0'));

        // 比较计算结果和尾部校验码是否一致。
        if (!(ID_CARD_VALID_CODES.charAt(sum % 11) == Character.toUpperCase(value.charAt(ID_CARD_WEIGHT.length)))) {
            throw new IllegalRequestParamException(name, "must match id-card validation code");
        }

        return value.toString();
    }

    /**
     * 生成用于指定自动操作的脚本。
     *
     * <p>
     * 自动操作的命名规则： {@literal [父元素ID]$[可点击元素的data-action]} 或者 {@literal [可点击元素的data-action]}。
     * 父元素ID和data-action都不能包含 {@literal "&~!<>()[]=\/}
     * 最后生成的脚本类似于：
     * </p>
     * <pre>
     *     document.querySelector('#panel [data-action=query]').click();
     * </pre>
     * <p>或</p>
     * <pre>
     *     document.querySelector('[data-action=query]').click();
     * </pre>
     * <p>脚本被保存到 {@code modelAndView} 的{@literal autoActionScript} 属性中。</p>
     *
     * @param action
     *         自动操作，如果该参数是{@code null}或者只包含空白字符则不生成任何脚本。
     * @param modelAndView
     *         模型和视图。
     * @throws IllegalRequestParamException
     *         如果参数{@code action} 没有包含{@literal data-action}部分或者包含了不允许出现的字符。
     */
    public static void processAutoAction(
            String action,
            ModelAndView modelAndView
    ) {
        action = stripToEmpty(action);
        if (action.isEmpty()) {
            return;
        }

        final var ta0 = splitByLast(action, '$');
        final var ctr = stripToEmpty(ta0[0]).toLowerCase();
        final var act = stripToEmpty(ta0[1]).toLowerCase();

        if (containsAny(ctr, ILLEGAL_AUTO_ACTION) || act.isEmpty() || containsAny(act, "'\"&~!<>()[]=\\/.")) {
            throw new IllegalRequestParamException("aa");
        }

        final String sel;
        if (!ctr.isEmpty()) {
            sel = "#" + ctr + " [data-action=" + act + "]";
        } else {
            sel = "[data-action=" + act + "]";
        }

        modelAndView.addObject("autoActionScript", "document.querySelector(\"" + sel + "\").click();");
    }
}
